Прогнозирование спроса с использованием моделей временных рядов¶
In [10]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from prophet import Prophet
import seaborn as sns
from sklearn.metrics import mean_absolute_error, mean_squared_error
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.arima.model import ARIMA
import plotly.express as px
Загрузка и предварительная обработка данных¶
In [11]:
# Загрузка данных
df = pd.read_csv('/home/s/Code/jupyter/02_store_item_demand_forecasting/input_data/train.csv')
In [12]:
# Просмотр первых нескольких строк данных
df.head()
Out[12]:
| date | store | item | sales | |
|---|---|---|---|---|
| 0 | 2013-01-01 | 1 | 1 | 13 |
| 1 | 2013-01-02 | 1 | 1 | 11 |
| 2 | 2013-01-03 | 1 | 1 | 14 |
| 3 | 2013-01-04 | 1 | 1 | 13 |
| 4 | 2013-01-05 | 1 | 1 | 10 |
In [13]:
# Общая информация о данных
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 913000 entries, 0 to 912999 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date 913000 non-null object 1 store 913000 non-null int64 2 item 913000 non-null int64 3 sales 913000 non-null int64 dtypes: int64(3), object(1) memory usage: 27.9+ MB
In [14]:
# Преобразование столбца 'date' в формат datetime
df['date'] = pd.to_datetime(df['date'])
In [15]:
# Уменьшение памяти за счёт преобразования типов данных
df['sales'] = df['sales'].astype('int32')
df['store'] = df['store'].astype('category')
df['item'] = df['item'].astype('category')
In [16]:
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 913000 entries, 0 to 912999 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date 913000 non-null datetime64[ns] 1 store 913000 non-null category 2 item 913000 non-null category 3 sales 913000 non-null int32 dtypes: category(2), datetime64[ns](1), int32(1) memory usage: 12.2 MB
In [17]:
# Проверка на наличие пропущенных значений
df.isnull().sum()
Out[17]:
date 0 store 0 item 0 sales 0 dtype: int64
In [18]:
# Основные статистические характеристики данных
df.describe(include='all')
Out[18]:
| date | store | item | sales | |
|---|---|---|---|---|
| count | 913000 | 913000.0 | 913000.0 | 913000.000000 |
| unique | NaN | 10.0 | 50.0 | NaN |
| top | NaN | 1.0 | 1.0 | NaN |
| freq | NaN | 91300.0 | 18260.0 | NaN |
| mean | 2015-07-02 11:59:59.999999744 | NaN | NaN | 52.250287 |
| min | 2013-01-01 00:00:00 | NaN | NaN | 0.000000 |
| 25% | 2014-04-02 00:00:00 | NaN | NaN | 30.000000 |
| 50% | 2015-07-02 12:00:00 | NaN | NaN | 47.000000 |
| 75% | 2016-10-01 00:00:00 | NaN | NaN | 70.000000 |
| max | 2017-12-31 00:00:00 | NaN | NaN | 231.000000 |
| std | NaN | NaN | NaN | 28.801144 |
Анализ временных рядов с ARIMA¶
In [20]:
# Группировка данных по дате
daily_sales = df.groupby('date')['sales'].sum().asfreq('D')
In [21]:
# Модель ARIMA
model_arima = ARIMA(daily_sales, order=(5,1,0))
model_arima_fit = model_arima.fit()
forecast_arima = model_arima_fit.get_forecast(steps=90)
forecast_arima_ci = forecast_arima.conf_int()
forecast_arima_index = pd.date_range(start=daily_sales.index[-1] + pd.Timedelta(days=1), periods=90)
In [22]:
# Визуализация прогноза ARIMA
plt.figure(figsize=(14, 7))
plt.plot(daily_sales, label='Наблюдаемые', color='blue')
plt.plot(forecast_arima_index, forecast_arima.predicted_mean, label='Прогноз', color='red')
plt.fill_between(forecast_arima_index,
forecast_arima_ci.iloc[:, 0],
forecast_arima_ci.iloc[:, 1], color='red', alpha=0.3)
plt.legend()
plt.title('Прогноз ARIMA', fontsize=16)
plt.xlabel('Дата', fontsize=14)
plt.ylabel('Продажи', fontsize=14)
plt.grid(True)
plt.axvline(x=daily_sales.index[-1], color='black', linestyle='--')
plt.show()
Декомпозиция временных рядов¶
In [13]:
# Декомпозиция временного ряда
decomposition = seasonal_decompose(daily_sales, model='additive')
In [14]:
# Настройка стиля и визуализации декомпозиции
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(14, 10), sharex=True)
ax1.plot(decomposition.observed, label='Наблюдаемые', color='blue')
ax1.legend(loc='upper left')
ax1.set_ylabel('Продажи')
ax2.plot(decomposition.trend, label='Тренд', color='green')
ax2.legend(loc='upper left')
ax2.set_ylabel('Тренд')
ax3.plot(decomposition.seasonal, label='Сезонность', color='orange')
ax3.legend(loc='upper left')
ax3.set_ylabel('Сезонность')
ax4.plot(decomposition.resid, label='Остаток', color='purple')
ax4.legend(loc='upper left')
ax4.set_ylabel('Остаток')
ax4.set_xlabel('Дата')
plt.suptitle('Декомпозиция временного ряда', fontsize=16)
plt.show()
Прогнозирование спроса с Prophet¶
In [15]:
# Подготовка данных для Prophet
prophet_df = daily_sales.reset_index().rename(columns={'date': 'ds', 'sales': 'y'})
In [16]:
# Создание и обучение модели Prophet
model_prophet = Prophet()
model_prophet.fit(prophet_df)
02:05:19 - cmdstanpy - INFO - Chain [1] start processing 02:05:19 - cmdstanpy - INFO - Chain [1] done processing
Out[16]:
<prophet.forecaster.Prophet at 0x729b8b748c50>
In [17]:
# Прогнозирование на квартал
future_prophet = model_prophet.make_future_dataframe(periods=90)
forecast_prophet = model_prophet.predict(future_prophet)
In [18]:
# Визуализация прогноза Prophet вручную
fig, ax = plt.subplots(figsize=(14, 7))
ax.plot(prophet_df['ds'], prophet_df['y'], 'k.', label='Наблюдаемые')
ax.plot(forecast_prophet['ds'], forecast_prophet['yhat'], 'r-', label='Прогноз')
ax.fill_between(forecast_prophet['ds'], forecast_prophet['yhat_lower'], forecast_prophet['yhat_upper'], color='red', alpha=0.3)
ax.axvline(x=daily_sales.index[-1], color='black', linestyle='--', label='Начало прогноза')
ax.set_title('Прогноз Prophet', fontsize=16)
ax.set_xlabel('Дата', fontsize=14)
ax.set_ylabel('Продажи', fontsize=14)
plt.legend()
plt.show()
Сравнение методов¶
In [23]:
# Разделение данных на обучающие и тестовые наборы для оценки точности
train = daily_sales.iloc[:-90]
test = daily_sales.iloc[-90:]
In [24]:
# Прогнозирование с ARIMA
model_arima = ARIMA(train, order=(5,1,0))
model_arima_fit = model_arima.fit()
forecast_arima = model_arima_fit.forecast(steps=90)
In [25]:
# Прогнозирование с Prophet
prophet_df = train.reset_index().rename(columns={'date': 'ds', 'sales': 'y'})
model_prophet = Prophet()
model_prophet.fit(prophet_df)
future_prophet = model_prophet.make_future_dataframe(periods=90)
forecast_prophet = model_prophet.predict(future_prophet)
# Выбор последних 90 дней из прогноза Prophet
forecast_prophet_test = forecast_prophet.set_index('ds').loc[test.index]['yhat']
02:30:19 - cmdstanpy - INFO - Chain [1] start processing 02:30:19 - cmdstanpy - INFO - Chain [1] done processing
In [26]:
# Оценка точности ARIMA и Prophet
mae_arima = mean_absolute_error(test, forecast_arima)
rmse_arima = np.sqrt(mean_squared_error(test, forecast_arima))
mae_prophet = mean_absolute_error(test, forecast_prophet_test)
rmse_prophet = np.sqrt(mean_squared_error(test, forecast_prophet_test))
print(f'ARIMA MAE: {mae_arima}, RMSE: {rmse_arima}')
print(f'Prophet MAE: {mae_prophet}, RMSE: {rmse_prophet}')
ARIMA MAE: 4501.768381606831, RMSE: 5422.566804873452 Prophet MAE: 1115.6539490878185, RMSE: 1525.7773911382196
In [27]:
# Определение самой точной модели
if mae_arima < mae_prophet:
print(f'Самая точная модель: ARIMA с MAE: {mae_arima} и RMSE: {rmse_arima}')
else:
print(f'Самая точная модель: Prophet с MAE: {mae_prophet} и RMSE: {rmse_prophet}')
Самая точная модель: Prophet с MAE: 1115.6539490878185 и RMSE: 1525.7773911382196
Анализ по категориям товаров и магазинам¶
Визуализация суммарных продаж по каждому магазину¶
In [28]:
store_sales = df.groupby('store', observed=False)['sales'].sum().reset_index()
plt.figure(figsize=(14, 8))
sns.barplot(x='store', y='sales', hue='store', data=store_sales, palette=sns.color_palette("dark"), dodge=False)
plt.title('Суммарные продажи по магазинам', fontsize=16)
plt.xlabel('Магазин', fontsize=14)
plt.ylabel('Суммарные продажи', fontsize=14)
plt.xticks(rotation=45)
plt.ticklabel_format(style='plain', axis='y') # Отображение оси Y в обычном формате
plt.grid(True)
plt.legend().set_visible(False) # Убираем легенду
plt.show()
Визуализация продаж товаров в одном из магазинов (например, магазин 1)¶
In [29]:
store_item_sales = df[df['store'] == 1].groupby('item', observed=False)['sales'].sum().reset_index()
store_item_sales['item'] = store_item_sales['item'].astype(str)
fig = px.bar(store_item_sales, x='item', y='sales', title='Продажи товаров в магазине 1', labels={'item':'Товар', 'sales':'Суммарные продажи'}, template='plotly_dark')
fig.update_traces(marker_color='rgb(100,149,237)', marker_line_color='rgb(8,48,107)', marker_line_width=1.5) # Используем более солидные цвета
fig.update_layout(title_font_size=16, xaxis_title_font_size=14, yaxis_title_font_size=14)
fig.show()
In [ ]:
In [ ]: